﻿using System;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace GraphDrawing2.V2.Concrete.Model
{
    /// <summary>
    /// Модель области отображения.
    /// </summary>
    public class ViewPortModel : IDisposable
    {
        const float MAX_SCALE = 5.0f;
        const float MIN_SCALE = 0.2f;

        /// <summary>
        /// Матрица преобразования координат из мировых в клиентские.
        /// </summary>
        readonly Matrix toClientTransform;

        /// <summary>
        /// Матрица преобразования координат из клиентских в мировые.
        /// </summary>
        readonly Matrix toWorldTransform;

        /// <summary>
        /// Массив для выполнения трансформаций.
        /// </summary>
        readonly PointF[] temp;

        /// <summary>
        /// Обозреватель модели.
        /// </summary>
        IViewPortObserver observer;

        /// <summary>
        /// Позиция области отображения в мировых координатах.
        /// </summary>
        public PointF Position { get; private set; }

        /// <summary>
        /// Мастшаб области отображения.
        /// </summary>
        public float Scale { get; private set; }

        /// <summary>
        /// Размер области отображения.
        /// </summary>
        public Size Size { get; private set; }

        public const float zoomSmoothness = 0.01f;
        float destinationScale;

        /// <summary>
        /// Создаёт новый экземпляр области отображения.
        /// </summary>
        public ViewPortModel()
        {
            Scale = 1.0f;
            destinationScale = 1.0f;
            temp = new PointF[4];
            toClientTransform = new Matrix();
            toWorldTransform = new Matrix();
        }

        public void SetObserver(IViewPortObserver observer)
        {
            this.observer = observer;
        }

        /// <summary>
        /// Увеличивает масштаб изображения.
        /// </summary>
        public void ZoomIn()
        {
            if (destinationScale >= MAX_SCALE)
                return;
            destinationScale += 0.1f;
            UpdateMatrices();
            if (observer != null)
                observer.ScaleChanged();
        }

        /// <summary>
        /// Уменьшает масштаб изображения.
        /// </summary>
        public void ZoomOut()
        {
            if (destinationScale <= MIN_SCALE)
                return;
            destinationScale -= 0.1f;
            UpdateMatrices();
            if (observer != null)
                observer.ScaleChanged();
        }

        /// <summary>
        /// Вызывается для обновления модели.
        /// </summary>
        public void Tick()
        {
            var delta = destinationScale - Scale;
            if (Math.Abs(delta) <= zoomSmoothness)
                return;
            if (delta > 0.0)
                Scale += zoomSmoothness;
            else
                Scale -= zoomSmoothness;
            UpdateMatrices();
            if (observer != null)
                observer.ScaleChanged();
        }

        /// <summary>
        /// Перемещает область изображения в новые координаты.
        /// </summary>
        /// <param name="newPosition">Новые координаты области отображения.</param>
        public void MoveTo(PointF newPosition)
        {
            Position = newPosition;
            UpdateMatrices();
            if (observer != null)
                observer.PositionChanged();
        }

        /// <summary>
        /// Перемещает область отображения на указанные значения.
        /// </summary>
        /// <param name="dx">Смещение по оси X.</param>
        /// <param name="dy">Смещение по оси Y.</param>
        public void Move(float dx, float dy)
        {
            var p = Position;
            Position = new PointF(p.X - dx, p.Y - dy);
            UpdateMatrices();
            if (observer != null)
                observer.PositionChanged();
        }

        /// <summary>
        /// Изменяет размер области отображения.
        /// </summary>
        /// <param name="newSize">Новый размер.</param>
        public void Resize(Size newSize)
        {
            Size = newSize;
            if (observer != null)
                observer.SizeChanged();
        }

        /// <summary>
        /// Преобразует координаты точки из локальных в мировые.
        /// </summary>
        /// <param name="p">Координаты точки.</param>
        public PointF ToWorld(PointF p)
        {
            return TransformOnePoint(p, toWorldTransform);
        }

        /// <summary>
        /// Преобразует координаты прямоугольника из локальных в мировые.
        /// </summary>
        public RectangleF ToWorld(RectangleF r)
        {
            return TransformRectangleF(r, toWorldTransform);
        }

        /// <summary>
        /// Преобразует координаты точки из мировых в локальные.
        /// </summary>
        /// <param name="p">Координаты точки.</param>
        public PointF ToClient(PointF p)
        {
            return TransformOnePoint(p, toClientTransform);
        }

        /// <summary>
        /// Возвращает матрицу преобразования для отображения объектов в мировых координатах.
        /// </summary>
        public Matrix CreateTransformation()
        {
            return toClientTransform.Clone();
        }

        /// <summary>
        /// Преобразует координаты точки с использованием указанной матрицы преобразования.
        /// </summary>
        /// <param name="p">Координаты точки.</param>
        /// <param name="m">Матрица преобразования.</param>
        PointF TransformOnePoint(PointF p, Matrix m)
        {
            temp[0] = p;
            m.TransformPoints(temp);
            return temp[0];
        }

        /// <summary>
        /// Преобразует координаты прямоугольника с помощью указанной матрицы.
        /// </summary>
        /// <param name="r">Исходный прямоугольник.</param>
        /// <param name="m">Матрица преобразования.</param>
        RectangleF TransformRectangleF(RectangleF r, Matrix m)
        {
            temp[0] = new PointF(r.Left, r.Top);
            temp[1] = new PointF(r.Right, r.Bottom);
            m.TransformPoints(temp);
            return new RectangleF(temp[0], new SizeF(temp[1]));
        }

        /// <summary>
        /// Устанавливает трансформации в матрицах.
        /// Порядок действий следующий:
        /// 1. Установить origin в центр области представления;
        /// 2. Выполнить масштабирование объектов в области.
        /// 3. Установить origin в исходное значение.
        /// 4. Переместить центр координат в текущее значение области (<see cref="Position"/>).
        /// Матрица преобразования в мировые координаты строится аналогичным образом, только в обратном порядке.
        /// </summary>
        void UpdateMatrices()
        {
            var centerx = Size.Width/2.0f;
            var centery = Size.Height/2.0f;

            toClientTransform.Reset();
            toClientTransform.Translate(centerx, centery);
            toClientTransform.Scale(Scale, Scale);
            toClientTransform.Translate(-centerx, -centery);
            toClientTransform.Translate(-Position.X, -Position.Y);

            toWorldTransform.Reset();
            toWorldTransform.Translate(centerx + Position.X, centery + Position.Y);
            var iscale = 1.0f/Scale;
            toWorldTransform.Scale(iscale, iscale);
            toWorldTransform.Translate(-centerx, -centery);

            var t1 = toClientTransform.Clone();
            t1.Multiply(toWorldTransform);
        }

        public void Dispose()
        {
            toClientTransform.Dispose();
            toWorldTransform.Dispose();
        }
    }
}